<?php
namespace WDB\GTO;
use WDB,
    WDB\Exception,
    WDB\Query,
    WDB\Query\Element;

class WsqlLanguage implements iLanguage
{
    private static $grammar = NULL;

    public static function parse($source, $symbol = 'Start')
    {
        return Parser::parse(__CLASS__, $source, $symbol);
    }

    public function getKeywords()
    {
        return 'SELECT|INSERT|UPDATE|DELETE|REPLACE|IGNORE|INTO|FROM|LEFT|RIGHT|OUTER|INNER|NATURAL|JOIN|USING|WHERE|IS|NOT|NULL'.
               '|GROUP BY|HAVING|LIMIT|ORDER BY|DESC|ASC|UNION|ALL|DISTINCT|AS|ON|DUPLICATE|KEY|VALUES|ODKU|SET|AND|'.
               'OR|NOT|XOR|NULL';
    }

    public static function quoteIdentifier($identifier)
    {
        if ($identifier instanceof Element\ColumnIdentifier)
        {
            $i = '';
            if ($identifier->schema)
            {
                $i .= self::quoteIdentifier($identifier->schema).'.';
            }
            if ($identifier->table)
            {
                $i .= self::quoteIdentifier($identifier->table).'.';
            }
            return $i.self::quoteIdentifier($identifier->column);
        }
        return '`'.str_replace('`', '``', $identifier).'`';
    }

    public static function quoteLiteral($literal)
    {
        if ($literal instanceof \DateTime)
        {
            return '\''.$literal->format('Y-m-d H:i:s').'\'';
        }
        return '\''.str_replace(array('\'', '\\'), array('\\\'', '\\\\'), $literal).'\'';
    }

    public function getTossTokens()
    {
        return array('whitespace', 'comment');
    }

    public function getTokenPatterns()
    {
        $identifier = '(?:
                [a-z_][a-z0-9_]* #unquoted identifier
                |
                `(?:[^`]|``)*` #` quoted identifier
                |
                "(?:[^`]|``)*" #" quoted identifier
            )';
        return array(
            'number'=>'[0-9]+(?:\\.[0-9]+)?',  #unquoted number
            'literal'=>"\'(?:\\\\.|[^'\\\\])*'",#quoted string
            'identifier'=>$identifier,
            'left_bracket'=>'\\(',
            'right_bracket'=>'\\)',
            'dot'=>'\\.',
            'comma'=>',',
            'star'=>'\\*',
            'expr_operator'=>'[-+/]',
            'equals'=>'=',
            'comparator'=>'(?:>=|>|<=|<|!=)',
            'comment'=>'--.*?(?:\n|$)',
            'whitespace'=>'\\s+',
            'raw_code'=>'{(?:[^}]|}})*}',
            'at'=>'@',
        );
    }

    public function getGrammar()
    {
        if (self::$grammar === NULL)
        {
            self::$grammar = new Grammar(file_get_contents(WDB\Config::read('grammarsdir').'wsql.llg'));
        }
        return self::$grammar;
    }

    public function objectFromStart($args)
    {
        if (isset($args['Select']))
        {
            return $this->objectFromSelect($args['Select']);
        }
        elseif (isset($args['Insert']))
        {
            return $this->objectFromInsert($args['Insert']);
        }
        elseif (isset($args['Update']))
        {
            return $this->objectFromUpdate($args['Update']);
        }
        elseif (isset($args['Delete']))
        {
            return $this->objectFromDelete($args['Delete']);
        }
        else
        {
            throw new Exception\CompileError("Unknown parse tree structure.");
        }
    }

    public function objectFromSelect($args)
    {
        $fields = array();
        $clist = $args;
        while (isset($clist['ColumnList']))
        {
            $clist = $clist['ColumnList'];
            $fields[] = new Element\SelectField($this->objectFromExpression($clist['Expression']), $clist['Alias'] ? $clist['Alias']['identifier']->content : NULL);
            $clist = $clist['FollowingColumns'];
        }
        $select = new Query\Select(
                $fields,
                $this->objectFromTableSource($args['TableSource']),
                $this->objectFromWhere($args['Where']),
                $this->objectFromOrderBy($args['OrderBy']),
                $this->objectFromGroupBy($args['GroupBy']),
                $this->objectFromWhere($args['Having'])
            );
        $this->setLimitAndOffset($select, $args['IsLimit']);
        $select->setDistinct((bool)$args['IsDistinct']);
        if ($args['IsUnion'])
        {
            $select->setUnion($this->objectFromSelect($args['IsUnion']['Select']), !isset($args['IsUnion']['UnionType']['distinct']));
        }
        return $select;
    }

    public function objectFromInsert($args)
    {
        $insert = new Query\Insert(
                $this->objectFromTableIdentifier($args['TableIdentifier']),
                $this->objectFromInsertClause($args['InsertClause'])
            );

        if ($args['Ignore'])
        {
            $insert->setMode(Query\Insert::IGNORE);
        }
        elseif($args['ODKU'])
        {
            if (isset($args['InsertMode']['replace']))
            {
                throw new Exception\CompileError('Replace query cannot contain ON DUPLICATE KEY UPDATE clause.');
            }
            $insert->setMode(Query\Insert::UPDATE);
            $insert->setODKUColumns($this->objectFromSetValues($args['ODKU']['SetValues']));
        }
        elseif(isset($args['InsertMode']['replace']))
        {
            $insert->setMode(Query\Insert::REPLACE);
        }
        else
        {
            $insert->setMode(Query\Insert::UNIQUE);
        }
        return $insert;
    }

    public function objectFromUpdate($args)
    {
        $update = new Query\Update(
                $this->objectFromTableIdentifier($args['TableIdentifier']),
                $this->objectFromSetValues($args['SetValues']),
                $this->objectFromWhere($args['Where'])
            );
        $update->setOrder($this->objectFromOrderBy($args['OrderBy']));
        $this->setLimitAndOffset($update, $args['IsLimit']);
        return $update;
    }

    public function objectFromDelete($args)
    {
        $delete = new Query\Delete(
                $this->objectFromTableIdentifier($args['TableIdentifier']),
                $this->objectFromWhere($args['Where'])
            );
        $delete->setOrder($this->objectFromOrderBy($args['OrderBy']));
        $this->setLimitAndOffset($delete, $args['IsLimit']);
        return $delete;
    }

    public function objectFromInsertClause($args)
    {
        if (isset($args['set']))
        {
            return $this->objectFromSetValues($args['SetValues']);
        }
        else
        {
            $columnIds = array();
            $idList = $args['IdentifierList'];
            do
            {
                $columnIds[] = $idList['identifier']->content;
                $idList = $idList['NextIdentifier'] ? $idList['NextIdentifier']['IdentifierList'] : FALSE;
            } while ($idList);
            $exprLists = $args['ExpressionLists'];
            if ($exprLists['NextExpressionLists'])
            {
                throw new Exception\Unimplemented('Multiple row insert is not yet implemented.');
            }
            $exprList = $exprLists['ExpressionList'];
            $columns = array();
            foreach ($columnIds as $columnId)
            {
                if (!$exprList) throw new Exception\CompileError('Column count and value count mismatch');
                $columns[$columnId] = $this->objectFromExpression($exprList['Expression']);
                $exprList = $exprList['NextExpression'] ? $exprList['NextExpression']['ExpressionList'] : FALSE;
            }
            if ($exprList) throw new Exception\CompileError('Column count and value count mismatch');
            return $columns;
        }
    }

    public function objectFromSetValues($args)
    {
        $values = array();
        do {
            $values[$args['identifier']->content] = $this->objectFromExpression($args['Expression']);
            $args = $args['NextSetValues'] ? $args['NextSetValues']['SetValues'] : FALSE;
        } while ($args);
        return $values;
    }

    public function objectFromTableSource($args)
    {
        $ts = array();
        do {
            $ts[] = $this->objectFromSingleTableSource($args['SingleTableSource']);
            if ($args['Join'])
            {
                $ts[] = array(
                    'type'=>end($args['Join']['JoinType'])->type,
                    'natural'=>(bool)$args['Join']['JoinNatural'],
                    'condition' => $args['Join']['JoinCondition']
                    );
                $args = $args['Join'];
            }
            else
            {
                break;
            }
        } while (TRUE);
        $result = $ts[0];
        for ($i = 1; $i < count($ts); $i += 2)
        {
            $op = $ts[$i];
            switch ($op['type'])
            {
                case 'left':
                    $type = Element\Join::LEFT;
                    break;
                case 'right':
                    $type = Element\Join::RIGHT;
                    break;
                case 'outer':
                    $type = Element\Join::OUTER;
                    break;
                default:
                    $type = Element\Join::INNER;
            }
            if ($op['natural'])
            {
                $condition = Element\Join::NATURAL;
            }
            elseif ($op['condition']['using'])
            {
                $idlist = $op['condition'];
                $list = array();
                do
                {
                    $list[] = self::unquoteIdentifier($idlist['IdentifierList']['identifier']->content);
                    $idlist = $idlist['IdentifierList']['NextIdentifier'];
                }while ($idlist);
                $condition = new Element\JoinUsing($list);
            }
            else
            {
                $condition = $this->objectFromCondition($op['condition']['Condition']);
            }
            $result = new Element\Join($result, $ts[$i+1], $condition, $type);
        }
        return $result;
    }

    public function objectFromSingleTableSource($args)
    {
        if (isset($args['TableIdentifier']))
        {
            $ti = $this->objectFromTableIdentifier($args['TableIdentifier']);
            if ($args['TableAlias'])
            {
                return Element\AliasedTableSource::create($ti, $args['TableAlias']['identifier']->content);
            }
            else
            {
                return $ti;
            }
        }
        if (isset($args['Select']))
        {
            return Element\AliasedTableSource::create($this->objectFromSelect($args['Select']), $args['identifier']->content);
        }
        throw new Exception\CompileError();
    }

    public function objectFromTableIdentifier($args)
    {
        $id = array(self::unquoteIdentifier($args['identifier']->content), NULL);
        if ($args['TDotId'])
        {
            array_unshift($id, self::unquoteIdentifier($args['TDotId']['identifier']->content));
        }
        return new Element\TableIdentifier(
                $id[0],
                $id[1]
            );
    }

    public static function unquoteIdentifier($identifier)
    {
        if (strlen($identifier) >= 2 && ($identifier[0] == '"' || $identifier[0] == '`'))
        {
            $identifier = str_replace($identifier[0].$identifier[0], $identifier[0], substr($identifier, 1, strlen($identifier)-2));
        }
        return $identifier;
    }
    public static function unquoteLiteral($literal)
    {
        return preg_replace('~(?<!\\\\)\\\\(\\\\\\\\)*\'~', '\\1\'', substr($literal, 1, strlen($literal)-2));
    }

    public function objectFromWhere($args)
    {
        if ($args === NULL) return NULL;
        return $this->objectFromCondition($args['Condition']);
    }

    public function objectFromCondition($args)
    {
        return $this->objectFromExpression($args['Expression']);
    }

    public function objectFromExpression($args)
    {
        $expressions = array();
        do {
            $expressions[] = $this->objectFromSingleExpression($args['SingleExpression']);
            if ($args['Operator'])
            {
                $expressions[] = end($args['Operator']['OperatorT'])->content;
                $args = $args['Operator']['Expression'];
            }
            else
            {
                break;
            }
        } while (TRUE);
        $expressions = $this->getInfixOps($expressions, array('*', '/'));
        $expressions = $this->getInfixOps($expressions, array('+', '-'));
        $expressions = $this->getInfixOps($expressions, array('>', '<', '>=', '<=', '=', '!='));
        $expressions = $this->getInfixOps($expressions, array('AND', 'OR', 'XOR'));
        if (count($expressions) > 1) throw new Exception\CompileError('Invalid operator.');
        return $expressions[0];
    }
    private function getInfixOps($expressions, array $ops)
    {
        $result = array();
        for ($i = 1; $i < count($expressions); $i += 2)
        {
            if (in_array($expressions[$i], $ops))
            {
                $chained = FALSE;
                if ($expressions[$i-1] instanceof Element\InfixOperator && $expressions[$i-1]->getOperator() == $expressions[$i])
                {
                    //chain to last operator
                    try {
                        $expressions[$i+1] = $expressions[$i-1]->addExpression($expressions[$i+1]);
                        $chained = TRUE;
                    }
                    //when an exception is thrown, chained is not set to TRUE so new expression operator branch is performed.
                    catch (Exception\InvalidOperation $ex) {}
                }
                if (!$chained)
                {
                    //new expression operator
                    $expressions[$i+1] = Element\InfixOperator::create(array($expressions[$i-1], $expressions[$i+1]), $expressions[$i]);
                }
            }
            else
            {
                $result[] = $expressions[$i-1];
                $result[] = $expressions[$i];
            }
        }
        $result[] = end($expressions);
        return $result;
    }

    public function objectFromSingleExpression($args)
    {
        if (isset($args['Literal']))
        {
            return $this->objectFromLiteral($args['Literal']);
        }
        if (isset($args['not']))
        {
            return Element\LogicOperator::lNot($this->objectFromExpression($args['Condition']));
        }
        if (isset($args['ColumnIdentifierOrFunction']))
        {
            if (isset($args['ColumnIdentifierOrFunction']['IoF']['left_bracket']) || $args['ColumnIdentifierOrFunction']['IsWDB'])
            {
                //function
                if (isset($args['ColumnIdentifierOrFunction']['IdentifierOrAll']['star']))
                {
                    throw new CompileError('Syntax error: function name cannot be a wildcard.');
                }
                $funcArgs = array();
                $argNT = $args['ColumnIdentifierOrFunction']['IoF']['HasArgs'];
                while ($argNT)
                {
                    $funcArgs[] = $this->objectFromExpression($argNT['ExpressionList']['Expression']);
                    $argNT = $argNT['ExpressionList']['NextExpression'];
                }
                return new Element\DBFunction(
                        $args['ColumnIdentifierOrFunction']['IdentifierOrAll']['identifier']->content,
                        $funcArgs,
                        !$args['ColumnIdentifierOrFunction']['IsWDB']
                    );
            }
            else
            {
                //column identifier
                $args['ColumnIdentifierOrFunction']['DotId2'] = $args['ColumnIdentifierOrFunction']['IoF']['DotId2'];
                return $this->objectFromColumnIdentifier($args['ColumnIdentifierOrFunction']);
            }
        }
        if (isset($args['left_bracket']))
        {
            return $this->objectFromExpression($args['Expression']);
        }
        if (isset($args['Select']))
        {
            return $this->objectFromSelect($args['Select']);
        }
        throw new Exception\CompileError("Unknown Condition rewrite rule used.");
    }
    public function objectFromLiteral($args)
    {
        if (isset($args['number']))
        {
            if (strpos($args['number']->content, '.') === FALSE)
                return new Element\Datatype\Integer ($args['number']->content);
            else
                return new Element\Datatype\Float ($args['number']->content);
        }
        if (isset($args['literal']))
        {
            return new Element\Datatype\String(self::unquoteLiteral($args['literal']->content));
        }
        if (isset($args['null']))
        {
            return new Element\Datatype\TNull;
        }
    }

    public function objectFromColumnIdentifier_getElm($arg)
    {
        if (isset($arg['identifier']))
        {
            return self::unquoteIdentifier($arg['identifier']->content);
        }
        else
        {
            return TRUE;
        }
    }
    public function objectFromColumnIdentifier($args)
    {

        $id = array($this->objectFromColumnIdentifier_getElm($args['IdentifierOrAll']), NULL, NULL);
        if ($args['DotId2'])
        {
            array_unshift($id, $this->objectFromColumnIdentifier_getElm($args['DotId2']['IdentifierOrAll']));
            if ($args['DotId2']['DotId'])
            {
                array_unshift($id, $this->objectFromColumnIdentifier_getElm($args['DotId2']['DotId']['IdentifierOrAll']));
            }
        }
        if ($id[1] === TRUE || $id[2] === TRUE) throw new Exception\CompileError("Invalid column identifier");
        return Element\ColumnIdentifier::create(
                $id[0],
                $id[1],
                $id[2]
            );
    }
    public function objectFromGroupBy($args)
    {
        $result = array();
        while ($args)
        {
            $result[] = $this->objectFromExpression($args['GroupByExpr']['Expression']);
            $args = $args['GroupByExpr']['NextGroupByEx'];
        }
        return $result;
    }
    private function setLimitAndOffset(Query\SUDQuery $query, $args)
    {
        if (!$args) return;
        $args = $args['Limit'];
        if ($args['LimitCount'])
        {
            $query->setLimit($args['LimitCount']['number']->content);
            $query->setOffset($args['number']->content);
        }
        else
        {
            $query->setLimit($args['number']->content);
            $query->setOffset(0);
        }
    }
    public function objectFromOrderBy($args)
    {
        $orders = array();
        if (!$args) return $orders;

        $args = $args['OrderByExpr'];
        while(TRUE)
        {
            $orders[] = new Element\OrderRule(
                    $this->objectFromExpression($args['Expression']),
                    isset($args['Dir']['desc']) ? Element\OrderRule::DESC : Element\OrderRule::ASC
                );
            if ($args['NextOrderByExpr'])
            {
                $args = $args['NextOrderByExpr']['OrderByExpr'];
            }
            else
            {
                return $orders;
            }
        }
    }
}